---
title: スクリプトとイベントハンドリング
description: >-
  ネイティブブラウザのJavaScript APIを使ってクライアントサイドのインタラクティブ性を
  Astroコンポーネントに追加する方法
i18nReady: true
---
import ReadMore from '~/components/ReadMore.astro'

標準的なHTMLの`<script>`タグを使用すれば、React・Svelte・Vue等の[UIフレームワーク](/ja/guides/framework-components/)を利用せずにインタラクティブ性をAstroコンポーネントへ追加できます。これによってブラウザ上で実行されるJavaScriptを送信してAstroコンポーネントに機能を追加できるようになります。

## クライアントサイドスクリプト

スクリプトは、イベントリスナーの追加やアナリティクスデータの送信、アニメーションの再生など、JavaScriptがウェブ上でできることすべてに使用できます。

```astro
<!-- src/components/ConfettiButton.astro -->
<button data-confetti-button>Celebrate!</button>

<script>
  // npmモジュールをインポートする。
  import confetti from 'canvas-confetti';

  // ページ上のコンポーネントのDOMを探す。
  const buttons = document.querySelectorAll('[data-confetti-button]');

  // ボタンがクリックされたときにconfettiを発火するイベントリスナーを追加する。
  buttons.forEach((button) => {
    button.addEventListener('click', () => confetti());
  });
</script>
```

デフォルトでは、Astroは`<script>`タグを処理してバンドルし、npmモジュールのインポートやTypeScriptの記述などをサポートします。

## Astroで`<script>`を使用する

`.astro`ファイル内に単数(もしくは複数)の`<script>`タグを追加することによってクライアントサイドJavascriptを追加できます。

下の例では、`<Hello />`コンポーネントをページに追加することでブラウザコンソール上にログメッセージを出力します。

```astro title="src/components/Hello.astro"
<h1>Welcome, world!</h1>

<script>
  console.log('Welcome, browser console!');
</script>
```

### スクリプトの処理

デフォルトで`<script`>タグはAstroで処理されます。

- すべてのインポートはバンドルされ、ローカルファイルやNodeモジュールをインポートできます。
- 処理されたスクリプトは、[`type="module"`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Modules)としてページの`<head>`に挿入されます。
- TypeScriptを対応しており、TypeScriptファイルもインポートできます
- コンポーネントがページに複数回使われている場合、スクリプトは一度だけ含まれます。

```astro title="src/components/Example.astro"
<script>
  // 処理される！ バンドルされる! TypeScriptサポート!
  // ローカルスクリプトやNodeモジュールのインポートも動作します。
</script>
```

`type="module"`属性は、ブラウザにスクリプトをJavaScriptモジュールとして扱わせます。これにはいくつかのパフォーマンス上のメリットがあります。

- レンダリングがブロックされません。モジュールスクリプトとその依存関係がロードされる間、ブラウザは残りのHTMLの処理を続けます。
- ブラウザはモジュールスクリプトを実行する前に、HTMLが処理されるのを待ちます。"load"イベントをリッスンする必要はありません。
- `async`と`defer`属性は不要です。モジュールスクリプトは常に延期されます。

:::note
`async`属性はレンダリングのブロックを防ぐため、通常のスクリプトにとって価値があります。しかし、モジュールスクリプトでは既にこの動作が含まれています。モジュールスクリプトに`async`を追加すると、ページが完全に読み込まれる前にモジュールスクリプトが実行されます。おそらくこれはあなたが望んでいることではありません。
:::

### 処理の対象外にする

Astroがスクリプトを処理しないようにするには、`is:inline`ディレクティブを追加します。

```astro title="src/components/InlineScript.astro" "is:inline"
<script is:inline>
  // 書かれたとおりにレンダリングされます。
  // ローカルインポートが解決されずに動作しません。
  // コンポーネント内の場合、コンポーネントが利用されている箇所全てに繰り返しレンダリングされます。
</script>
```

:::note
Astroは、状況によってはスクリプトタグを処理しません。特に、`<script>`タグに`type="module"`や`src`以外の属性を追加すると、Astroはそのタグを`is:inline`ディレクティブがあるかのように扱います。スクリプトがJSX式で記述されている場合も同様です。
:::

<ReadMore>`<script>` タグで利用可能なディレクティブについてもっと知りたい場合は、[ディレクティブリファレンス](/ja/reference/directives-reference/#script--style-directives)ページを参照してください。</ReadMore>

### ページにJavaScriptファイルを含める

スクリプトを`.js`や`.ts`ファイルのように分離して書きたい場合や、他のサーバーから外部スクリプトを参照したい場合には`<script>`タグの`src`属性の参照を使えます。

#### ローカルスクリプトのインポート

**これを使うケース:** スクリプトが`src`に存在している時。

Astroは[スクリプトの処理のルール](#スクリプトの処理)に従って、ビルド、最適化、そしてページ内にスクリプトを追加します。

```astro title="src/components/LocalScripts.astro"
<!-- `src/scripts/local.js`への相対パス -->
<script src="../scripts/local.js"></script>

<!-- このローカルのTypeScriptファイルも動作します。 -->
<script src="./script-with-types.ts"></script>
```

#### 外部スクリプトをロードする

**これを使うケース:** JavaScriptファイルが`public`ディレクトリに存在しているか、CDNの時。

プロジェクトの`src`フォルダ以外のスクリプトをロードするには、`is:inline`ディレクティブを含めます。このアプローチでは、上記のようにスクリプトをインポートする際にAstroが提供しているJavaScriptの処理・バンドル・最適化はスキップされます

```astro title="src/components/ExternalScripts.astro" "is:inline"
<!-- `public/my-script.js`スクリプトへの絶対パス -->
<script is:inline src="/my-script.js"></script>

<!-- リモートサーバー上にあるスクリプトへの完全なURL -->
<script is:inline src="https://my-analytics.com/script.js"></script>
```

## 共通スクリプトパターン

### `onclick`や他のイベントをハンドリングする

いくつかのUIフレームワークでは`onclick={...}`(React/Preact)や`@click="..."`(Vue)のように独自の構文でイベントを処理しています。Astroは標準的なHTMLにより近く、イベントのために独自の構文は利用しません。

その代わりに、ユーザーのインタラクションをハンドリングするために`<script>`タグ内に[`addEventListener`](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener)を利用します。

```astro title="src/components/AlertButton.astro"
<button class="alert">Click me!</button>

<script>
  // ページ内から`alert`クラスを持つすべてのボタンを探す。
  const buttons = document.querySelectorAll('button.alert');

  // 各ボタンがクリックされたときのハンドリング
  buttons.forEach((button) => {
    button.addEventListener('click', () => {
      alert('Button was clicked!');
    });
  });
</script>
```

:::note
ページに複数の`<AlertButton />`コンポーネントが存在する場合でも、Astroは複数回スクリプトを実行しません。スクリプトはページに1つしか含まれないようにバンドルされます。`querySelectorAll` を利用すると、このスクリプトはページ上で`alert`クラスを持つすべてのボタンにイベントリスナーをアタッチします。
:::

### カスタム要素を持つWebコンポーネント

Webコンポーネント標準を使うことで独自の動作をするHTML要素を作成できます。`.astro`コンポーネントで[カスタム要素](https://developer.mozilla.org/ja/docs/Web/Web_Components/Using_custom_elements)を定義するとUIフレームワークライブラリを利用しなくてもインタラクティブなコンポーネントを作ることができます。

下の例では、ハートボタンがクリックされた回数を記録し、その最新のカウント数を`<span>`に更新する`<astro-heart>`HTML要素を新しく定義しています。

```astro title="src/components/AstroHeart.astro"
<!-- カスタム要素 "astro-heart" でコンポーネント要素を囲みます。 -->
<astro-heart>
  <button aria-label="Heart">💜</button> × <span>0</span>
</astro-heart>

<script>
  // 新しいタイプのHTML要素の動作を定義する
  class AstroHeart extends HTMLElement {
    constructor() {
			super();
      let count = 0;

      const heartButton = this.querySelector('button');
      const countSpan = this.querySelector('span');

      // ボタンがクリックされるごとにカウントを更新する。
			heartButton.addEventListener('click', () => {
        count++;
        countSpan.textContent = count.toString();
      });
		}
  }

  // <astro-heart>要素としてAstroHeartクラスを利用することをブラウザに教える
  customElements.define('astro-heart', AstroHeart);
</script>
```

ここでカスタム要素を利用するメリットは2つあります。

1. `document.querySelector()`を使って全ページを検索する変わりに、`this.querySelector()`を使えば検索範囲が現在のカスタム要素のインスタンスに限られます。これにより、一回で一つのコンポーネントだけを操作しやすくなります。
2. `<script>`は一度だけしか実行されませんが、ブラウザはページ上の`<astro-heart>`を見つける度にカスタム要素の`constructor()`メソッドを実行します。これにより、ページで複数回コンポーネントを使用する場合でも一つのコンポーネントを安全に記述できます。

<ReadMore>[web.devの再利用可能なウェブコンポーネントガイド](https://web.dev/articles/custom-elements-v1?hl=ja)と[MDNのカスタム要素の紹介](https://developer.mozilla.org/ja/docs/Web/Web_Components/Using_custom_elements)で、カスタム要素について詳しく学べます。</ReadMore>

### フロントマター変数をスクリプトに渡す

Astroコンポーネントでは、`---`フェンスの間にある[フロントマター](/ja/basics/astro-components/#コンポーネントスクリプト)のコードはサーバー上で実行され、ブラウザでは利用できません。サーバーからクライアントへ変数を渡すには、変数を保存しておきJavaScriptがブラウザで実行されたときに読み込む必要があります。

これを実現するための1つの手段は[`data-*` attributes](https://developer.mozilla.org/ja/docs/Learn/HTML/Howto/Use_data_attributes)を利用してHTML出力に変数の値を保存する方法です。カスタム要素を含むスクリプトはブラウザ上にHTMLがロードされると要素の`dataset`を利用することで、この属性を読み込むことができます。

下の例では、`message` propを`data-message`属性に保存されているので、カスタム要素が`this.dataset.message`を読み込みブラウザ上で値を取得しています。

```astro title="src/components/AstroGreet.astro" {2} /data-message={.+}/ "this.dataset.message"
---
const { message = 'Welcome, world!' } = Astro.props;
---

<!-- message propをdata属性に保存する -->
<astro-greet data-message={message}>
  <button>Say hi!</button>
</astro-greet>

<script>
  class AstroGreet extends HTMLElement {
    constructor() {
			super();

      // data属性からmessageを読み込む
      const message = this.dataset.message;
      const button = this.querySelector('button');
      button.addEventListener('click', () => {
        alert(message);
      });
		}
  }

  customElements.define('astro-greet', AstroGreet);
</script>
```

これでコンポーネントは何度でも利用でき、異なるmessageを表示できます

```astro title="src/pages/example.astro"
---
import AstroGreet from '../components/AstroGreet.astro';
---

<!-- messageの初期値「Welcome, world!」を利用する -->
<AstroGreet />

<!-- propsとしてカスタムmessageを渡す -->
<AstroGreet message="Lovely day to build components!" />
<AstroGreet message="Glad you made it! 👋" />
```

:::tip[知っていましたか？]
この方法はReactなどのUIフレームワークを用いて書かれたコンポーネントにpropsを渡す際にAstroが裏で行っていることです！`client:*`ディレクティブを持つコンポーネントに対して、Astroは`<astro-island>`カスタム要素に`props`属性を作成してサーバーサイドの`props`をHTML出力結果に保存します。
:::
